﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using CILADA.Core.Graph;
using QuickGraph;
using Mono.Cecil.Cil;

namespace CILADA.Core.Analysis
{
    class DependencyGraphBuilder
    {
        public AnalysisModes AnalysisMode { get; set; }
        public IGraphAdapter<IMember, TaggedEdge<IMember, EdgeType>> AssemblyGraph { get; set; }

        private Stack<ActiveType> currentType;
        private string currentAssembly;

        public DependencyGraphBuilder(AnalysisModes analysisMode)
        {
            this.AnalysisMode = analysisMode;
            this.AssemblyGraph = new QuickGraphAdapter<IMember, TaggedEdge<IMember, EdgeType>>();

            currentType = new Stack<ActiveType>();
            currentAssembly = null;
        }

        public void AnalyseAssembly(AssemblyDefinition assembly, string assemblyName)
        {
            currentAssembly = assemblyName;
            // foreach module
            foreach (ModuleDefinition md in assembly.Modules)
            {
                if (!"<Module>".Equals(md.Name))
                {
                    foreach (TypeDefinition td in md.Types)
                    {
                        // TODO add an exclusion for the "<Module>" type ?
                        AnalyseType(td);
                    }
                }
            }

            currentAssembly = null;
            //Console.WriteLine("Finished analysing in {0} mode",this.AnalysisMode);
        }

        protected void AnalyseType(TypeDefinition td)
        {
            if (td == null) return;

            TypeVertex tv = td.ToTypeVertex(currentAssembly);
            AssemblyGraph.AddVertex(tv);

            // add the current type onto the stack
            ActiveType at = new ActiveType(tv, td);
            currentType.Push(at);

            // get base type
            if (td.BaseType != null)
            {
                AddEdgeForType(tv, td.BaseType, EdgeType.extends);
            }

            // interfaces
            foreach (TypeReference inter in td.Interfaces)
            {
                AddEdgeForType(tv, inter, EdgeType.implements);
            }

            // extract type constraints
            if (td.HasGenericParameters)
            {
                foreach (GenericParameter gp in td.GenericParameters)
                {
                    foreach (TypeReference c in gp.Constraints)
                    {
                        AddEdgeForType(tv, c, EdgeType.uses);
                    }
                }
            }

            // nested types
            foreach (TypeDefinition ntd in td.NestedTypes)
            {
                AnalyseType(ntd);
            }

            foreach (PropertyDefinition pd in td.Properties)
            {
                AnalyseProperty(pd);
            }

            foreach (MethodDefinition md in td.Methods)
            {
                AnalyseMethod(md);
            }

            foreach (EventDefinition ed in td.Events)
            {
                AnalyseEvent(ed);
            }

            // remove the type that has finished being analysed from the stack
            currentType.Pop();
        }

        private void AnalyseEvent(EventDefinition ed)
        {
            // FIXME - complete event analysis.
            AddEdgeForType(currentType.Pop().Vertex, ed.EventType, EdgeType.uses);
        }

        protected void AnalyseMethod(MethodDefinition md)
        {
            if (md == null) return;
            TypeDefinition curTypeDef = currentType.Peek().Type;
            IMember curTypeVertex = currentType.Peek().Vertex;

            MethodVertex mv = md.ToMethodVertex();

            if (this.AnalysisMode == AnalysisModes.Detailed)
            {
                AssemblyGraph.AddVertex(mv);
                CreateTaggedEdgeAndAddToGraph(mv, currentType.Peek().Vertex, EdgeType.belongsto);
            }

            // add the type the method belongs to as a dependency, if it is not the current type
            if (!md.DeclaringType.FullName.Equals(curTypeDef.FullName))
            {
                AddEdgeForType(curTypeVertex, md.DeclaringType, EdgeType.uses);
            }

            // get method return type
            TypeVertex returnTypeVertex = null;
            TypeDefinition returnType = null;
            bool resolvedReturnType = md.ReturnType.TryResolveTypeReference(out returnType);
            if (resolvedReturnType)
            {
                returnTypeVertex = returnType.ToTypeVertex();
            }
            else
            {
                returnTypeVertex = md.ReturnType.ToTypeVertex();
            }

            IMember vertex = (this.AnalysisMode == AnalysisModes.ClassesOnly) ?
                        curTypeVertex : mv;

            // is this if statement needed? Can a return type ever be a generic instance.
            // what about when a method returns a generic type T ?
            if (resolvedReturnType && returnType.IsGenericInstance)
            {
                GenericInstanceType gt = md.ReturnType as GenericInstanceType;
                foreach (TypeReference ga in gt.GenericArguments)
                {
                    AddEdgeForType(vertex, ga, EdgeType.guses);

                    // dont need to find type constraints here, as they will be found by analysing the 
                    // type if it is in the set of assemblies being analysed, or will not be relevant if
                    // if it is not in this set.
                }
            }
            CreateTaggedEdgeAndAddToGraph(vertex, returnTypeVertex, EdgeType.hasreturnType);


            // if the method is on a generic instance, and uses the types, try to resolve them.
            if (md.IsGenericInstance || md.HasGenericParameters)
            {
                if (currentType.Peek().Type.IsGenericInstance)
                {
                    GenericInstanceType gt = (curTypeDef as TypeReference) as GenericInstanceType;
                    foreach (TypeReference ga in gt.GenericArguments)
                    {
                        AddEdgeForType(curTypeVertex, ga, EdgeType.guses);

                        // dont need to find type constraints here, as they will be found by analysing the 
                        // type if it is in the set of assemblies being analysed, or will not be relevant if
                        // if it is not in this set.
                    }
                }
            }

            // are these only the non generic params?
            // get method parameter types
            foreach (ParameterDefinition pd in md.Parameters)
            {
                if (!pd.ParameterType.IsGenericParameter)
                {
                    // only add it if is is not of the same type.
                    if (!pd.ParameterType.FullName.Equals(curTypeDef.FullName))
                    {
                        AddEdgeForType(curTypeVertex, pd.ParameterType, EdgeType.uses);
                    }
                }
            }
            // print out a list of parameters and any available type data for checking.

            // get types used in method body
            AnalyseMethodBody(mv, md);
        }

        private void AnalyseProperty(PropertyDefinition pd)
        {
            if (pd == null) return;

            AttributeVertex av = pd.ToAttributeVertex();

            if (this.AnalysisMode == AnalysisModes.Detailed)
            {
                AssemblyGraph.AddVertex(av);
                CreateTaggedEdgeAndAddToGraph(currentType.Peek().Vertex, av, EdgeType.hasattribute);
            }

            // add the type, if it is known at compile time.
            if (!pd.PropertyType.IsGenericParameter) // FIXME will this ever be entered?
            {
                if (!pd.PropertyType.FullName.Equals(currentType.Peek().Type.FullName))
                {
                    AddEdgeForType(currentType.Peek().Vertex, pd.PropertyType, EdgeType.uses);
                }
                
            }

            // get types used in get method
            AnalyseMethod(pd.GetMethod);

            // get types used in set method
            AnalyseMethod(pd.SetMethod);
        }

        private void AnalyseMethodBody(MethodVertex mv, MethodDefinition md)
        {
            // what params are needed for this
            if (md == null) return;
            if (mv == null)
            {
                mv = md.ToMethodVertex();
            }

            if (md.HasBody && md.Body != null)
            {
                SequencePoint currentSequencePoint = null;
                foreach (Instruction i in md.Body.Instructions)
                {
                    currentSequencePoint = i.SequencePoint ?? currentSequencePoint;

                    // find types.
                    if (i.Operand != null)
                    {
                        if (i.OpCode.OperandType == OperandType.InlineType)
                        {
                            TypeReference tr = i.Operand as TypeReference;

                            if (!tr.FullName.Equals(currentType.Peek().Type.FullName))
                            {
                                // FIXME what if tr is a generic type?
                                // how to tell the difference between a generic parameter that is defined in 
                                // the class, and one that is taken from the classes generic params?
                                // simply check if the parent type has any generic params?
                                EdgeType edgeType = (currentType.Peek().Type.HasGenericParameters &&
                                    tr.HasGenericParameters && tr.IsGenericParameter)
                                    ? EdgeType.guses : EdgeType.uses;

                                if (this.AnalysisMode == AnalysisModes.Detailed)
                                {
                                    AddEdgeForType(mv, tr, edgeType);
                                }
                                else
                                {
                                    AddEdgeForType(currentType.Peek().Vertex, tr, edgeType);
                                }
                            }
                        }
                        else
                        {
                            if (i.Operand is MethodReference)
                            {
                                MethodReference mr = (MethodReference)i.Operand;

                                // FIXME what if mr is a generic method?
                                // FIXME what information should be obtained from the method here? 
                                //       return type, parameters, generic constaints, etc...

                                if (this.AnalysisMode == AnalysisModes.Detailed)
                                {
                                    CreateTaggedEdgeAndAddToGraph(mv, mr.ToMethodVertex(), EdgeType.invokes);
                                }
                                else
                                {
                                    if (!mr.DeclaringType.FullName.Equals(currentType.Peek().Type.FullName))
                                    {
                                        AddEdgeForType(currentType.Peek().Vertex, mr.DeclaringType, EdgeType.uses);
                                    }
                                }
                            }
                        }
                    }

                    // should also check InlineToken, re http://go-mono.com/forums/#nabble-td1551209
                }
            }
        }

        protected void CreateTaggedEdgeAndAddToGraph(IMember source, IMember target, EdgeType edgeType)
        {
            if (source.FullName.Equals(target.FullName) && edgeType.Equals(EdgeType.uses))
            {
                // cannot add an edge of type uses from a type to itself
                return;
            }

            TaggedEdge<IMember, EdgeType> e = new TaggedEdge<IMember, EdgeType>(source, target, edgeType);
            AssemblyGraph.AddEdge(e);
        }

        protected void AddEdgeForMethod(IMember mv, MethodReference mr, EdgeType edgeType)
        {
            IMember vertex = null;
            if (AssemblyGraph.ContainsVertex(v => v.FullName.Equals(mr.FullName), out vertex))
            {
                CreateTaggedEdgeAndAddToGraph(mv, vertex, edgeType);
            }
            else
            {
                CreateTaggedEdgeAndAddToGraph(mv, mr.ToMethodVertex(), edgeType);
            }
        }

        protected void AddEdgeForType(IMember mv, TypeReference tr, EdgeType edgeType)
        {
            IMember vertex = null;
            if (AssemblyGraph.ContainsVertex(v => v.FullName.Equals(tr.FullName), out vertex))
            {
                CreateTaggedEdgeAndAddToGraph(mv, vertex, edgeType);
            }
            else
            {
                TypeDefinition td = null;
                if (tr.TryResolveTypeReference(out td))
                {
                    CreateTaggedEdgeAndAddToGraph(mv, td.ToTypeVertex(), edgeType);
                }
                else
                {
                    CreateTaggedEdgeAndAddToGraph(mv, tr.ToTypeVertex(), edgeType);
                }
            }
        }
    }
}
